﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Maths.CSharp
{
    /// <example>Math.Sin(0)</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class TrigonometricConstantsDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        public const string DiagnosticId = "TrigonometricConstants";

        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            DiagnosticId,
            "Trigonometric function evaluates to an exact constant",
            "Trigonometric function evaluates to constant, can be simplified.",
            "Performance",
            DiagnosticSeverity.Warning
        );

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest
        {
            get
            {
                return ImmutableArray.Create(SyntaxKind.InvocationExpression);
            }
        }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var invocation = node as InvocationExpressionSyntax) != null)
            if ((var method = semanticModel.GetSymbolInfo(invocation.Expression, cancellationToken).Symbol as IMethodSymbol) != null)
            {
                // check if it is from Math class
                if (!method.ConstructedFrom.ContainingType.Equals(semanticModel.Compilation.GetTypeByMetadataName(typeof(Math).FullName)))
                    return;

                // get argument
                if (invocation.ArgumentList.Arguments.Count != 1)
                    return;

                ExpressionSyntax expr = invocation.ArgumentList.Arguments.Single().Expression;
                Optional<object> constant = semanticModel.GetConstantValue(expr, cancellationToken);

                if (!constant.HasValue)
                    return;

                double value = Convert.ToDouble(constant.Value);
                

                // Sine
                if (method.Name == "Sin")
                {
                    if (Math.IEEERemainder(value, Math.PI / 2) == 0)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }

                // Cosine
                else if (method.Name == "Cos")
                {
                    if (Math.IEEERemainder(value, Math.PI / 2) == 0)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }

                // Tangent
                else if (method.Name == "Tan")
                {
                    if (Math.IEEERemainder(value - Math.PI / 2, Math.PI) == 0)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }

                // Cotangent
                else if (method.Name == "Cot") // Math.Cot does not exists
                {
                    if (Math.IEEERemainder(value, Math.PI) == 0)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }


                // Inverse Sine
                else if (method.Name == "Asin")
                {
                    if (value == 0 || Math.Abs(value) == 1)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }

                // Inverse Cosine
                else if (method.Name == "Acos")
                {
                    if (value == 0 || Math.Abs(value) == 1)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }

                // Inverse Tangent
                else if (method.Name == "Atan")
                {
                    if (value == 0)
                        addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
                }
            }
        }
    }

    [ExportCodeFixProvider(TrigonometricConstantsDiagnosticAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class TrigonometricConstantsCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            return new[] { TrigonometricConstantsDiagnosticAnalyzer.DiagnosticId };
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);

            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // Find the local declaration identified by the diagnostic.
            var invocation = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().First();

            // return a code action that will invoke the fix.
            return new[] { CodeAction.Create("Simplify trigonometric function to exact constant", c => Fix(document, invocation, c)) };
        }

        private async Task<Document> Fix(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            SyntaxNode syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken);
            SemanticModel model = await document.GetSemanticModelAsync(cancellationToken);

            // get simplified value
            SyntaxNode replacement = GetSimplifiedValue(model, invocation, cancellationToken)
                .WithAdditionalAnnotations(Formatter.Annotation);

            SyntaxNode newRoot = syntaxRoot.ReplaceNode(invocation, replacement);
            return document.WithSyntaxRoot(newRoot);
        }

        private ExpressionSyntax GetSimplifiedValue(SemanticModel model, InvocationExpressionSyntax invocation, CancellationToken cancellationToken)
        {
            if ((var method = model.GetSymbolInfo(invocation.Expression, cancellationToken).Symbol as IMethodSymbol) != null)
            {
                ExpressionSyntax expr = invocation.ArgumentList.Arguments.Single().Expression;
                Optional<object> constant = model.GetConstantValue(expr, cancellationToken);

                double value = Convert.ToDouble(constant.Value);


                // Sine
                if (method.Name == "Sin")
                {
                    if (Math.IEEERemainder(value, Math.PI) == 0)
                        return CreateZero();

                    else if (Math.IEEERemainder(value - Math.PI / 2, 2 * Math.PI) == 0)
                        return CreateOne();

                    else if (Math.IEEERemainder(value - 3 * Math.PI / 2, 2 * Math.PI) == 0)
                        return CreateMinusOne();
                }

                // Cosine
                else if (method.Name == "Cos")
                {
                    if (Math.IEEERemainder(value, Math.PI / 2) == 0)
                        return CreateZero();

                    else if (Math.IEEERemainder(value, 2 * Math.PI) == 0)
                        return CreateOne();

                    else if (Math.IEEERemainder(value - Math.PI, 2 * Math.PI) == 0)
                        return CreateMinusOne();
                }

                // Tangent
                else if (method.Name == "Tan")
                {
                    if (Math.IEEERemainder(value - Math.PI / 2, Math.PI) == 0)
                        return CreateZero();
                }

                // Cotangent
                else if (method.Name == "Cot") // Math.Cot does not exists
                {
                    if (Math.IEEERemainder(value, Math.PI) == 0)
                        return CreateZero();
                }


                // Inverse Sine
                else if (method.Name == "Asin")
                {
                    if (value == 0)
                        return CreateZero();
                    else if (value == 1)
                        return CreatePiDividedByTwo();
                    else if (value == -1)
                        return CreateMinusPiDividedByTwo();
                }

                // Inverse Cosine
                else if (method.Name == "Acos")
                {
                    if (value == 0)
                        return CreatePiDividedByTwo();
                    else if (value == 1)
                        return CreateZero();
                    else if (value == -1)
                        return CreatePi();
                }

                // Inverse Tangent
                else if (method.Name == "Atan")
                {
                    if (value == 0)
                        return CreateZero();
                }
            }

            throw new NotSupportedException();
        }


        static ExpressionSyntax CreateZero()
        {
            return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression,
                SyntaxFactory.Literal(0)
            );
        }

        static ExpressionSyntax CreateOne()
        {
            return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression,
                SyntaxFactory.Literal(1)
            );
        }

        static ExpressionSyntax CreateMinusOne()
        {
            return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression,
                SyntaxFactory.Literal(-1)
            );
        }

        static ExpressionSyntax CreatePiDividedByTwo()
        {
            return SyntaxFactory.BinaryExpression(SyntaxKind.DivideExpression,
                left: CreatePi(),
                right: SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression,
                    SyntaxFactory.Literal(2)
                )
            );
        }

        private static MemberAccessExpressionSyntax CreatePi()
        {
            return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                SyntaxFactory.QualifiedName(SyntaxFactory.ParseName("System"), SyntaxFactory.IdentifierName("Math")),
                SyntaxFactory.IdentifierName("PI")
            )
                .WithAdditionalAnnotations(Simplifier.Annotation);
        }

        static ExpressionSyntax CreateMinusPiDividedByTwo()
        {
            return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.UnaryMinusExpression,
                CreatePiDividedByTwo()
            );
        }
    }
}
